﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Net.WebSockets;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;

namespace WebCamera.WebSocket
{
    public class WSCommand
    {
        [JsonProperty("c")]
        public string Cmd { get; set; }
        [JsonProperty("ch")]
        public string Channel { get; set; }
        [JsonProperty("a")]
        public string Args { get; set; }
    }
    public static class WebSocketExtensions
    {
        public static IApplicationBuilder MapWebSocketManager(this IApplicationBuilder app, PathString path, WebSocketManagerMiddleware.WebSocketHandler handler)
        {
            return app.Map(path, (_app) => _app.UseMiddleware<WebSocketManagerMiddleware>(handler));
        }
        public static IServiceCollection AddWebSocketManager(this IServiceCollection services)
        {
            services.AddTransient<WebSocketConnectionManager>();

            var types = Assembly.GetEntryAssembly().GetTypes().Where(p => p.IsAbstract == false && typeof(WebSocketManagerMiddleware.WebSocketHandler).IsAssignableFrom(p)).ToList();
            foreach (var type in types)
            {
                services.AddSingleton(type);
            }

            return services;
        }
    }

    public class WebSocketConnectionManager
    {
        public async Task CloseSocket(System.Net.WebSockets.WebSocket socket)
        {
            await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
        }
    }

    public class WebSocketManagerMiddleware
    {
        private readonly RequestDelegate _next;
        private WebSocketHandler _webSocketHandler { get; set; }

        public WebSocketManagerMiddleware(RequestDelegate next,
                                          WebSocketHandler webSocketHandler)
        {
            _next = next;
            _webSocketHandler = webSocketHandler;
        }

        public async Task Invoke(HttpContext context)
        {
            if (!context.WebSockets.IsWebSocketRequest)
                return;

            var socket = await context.WebSockets.AcceptWebSocketAsync();
            _webSocketHandler.OnConnected(socket);

            await Receive(socket, async (result, buffer) =>
            {
                if (result.MessageType == WebSocketMessageType.Text)
                {
                    await _webSocketHandler.ReceiveAsync(socket, result, buffer);
                    return;
                }

                else if (result.MessageType == WebSocketMessageType.Close)
                {
                    await _webSocketHandler.OnDisconnected(socket);
                    return;
                }
                else
                {
                    Debug.WriteLine($"unknown message type: {result.MessageType}");
                    return;
                }
            });

            //TODO - investigate the Kestrel exception thrown when this is the last middleware
            //await _next.Invoke(context);
        }

        private async Task Receive(System.Net.WebSockets.WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage)
        {
            try
            {
                var buffer = new byte[1024 * 4];

                while (socket.State == WebSocketState.Open)
                {
                    var result = await socket.ReceiveAsync(buffer: new ArraySegment<byte>(buffer),
                                                           cancellationToken: CancellationToken.None);

                    handleMessage(result, buffer);
                }
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex);
            }
        }

        public abstract class WebSocketHandler
        {
            public WebSocketConnectionManager WebSocketConnectionManager { get; set; }

            public WebSocketHandler(WebSocketConnectionManager webSocketConnectionManager)
            {
                WebSocketConnectionManager = webSocketConnectionManager;
            }

            public virtual void OnConnected(System.Net.WebSockets.WebSocket socket)
            {
            }

            public virtual async Task OnDisconnected(System.Net.WebSockets.WebSocket socket)
            {
            }

            public async Task SendMessageAsync(System.Net.WebSockets.WebSocket socket, string message)
            {
                if (socket.State != WebSocketState.Open)
                    return;
                var bytes = Encoding.UTF8.GetBytes(message);
                await socket.SendAsync(buffer: new ArraySegment<byte>(array: bytes, offset: 0, count: bytes.Length), messageType: WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None);
            }
            protected abstract void StartCamera(System.Net.WebSockets.WebSocket socket, string chn, string args);
            protected abstract void StopCamera(System.Net.WebSockets.WebSocket socket);
            public virtual Task ReceiveAsync(System.Net.WebSockets.WebSocket socket, WebSocketReceiveResult result, byte[] buffer)
            {
                switch (result.MessageType)
                {
                    case WebSocketMessageType.Text:
                    {
                        var str = Encoding.UTF8.GetString(buffer, 0, result.Count);
                        if (str == "keepalive")
                            return Task.CompletedTask;
                        Debug.WriteLine($"{str}");
                        var cmd = JsonConvert.DeserializeObject<WSCommand>(str);
                        if (cmd.Cmd == "open")
                        {
                            StartCamera(socket, cmd.Channel, cmd.Args);
                        }
                        else if (cmd.Cmd == "close")
                        {
                            StopCamera(socket);
                        }
                        }
                        break;
                }
                return Task.CompletedTask;
            }
        }
    }
}
